package agents;

import game.Tree;
import game.TreeBuilder;
import jade.core.AID;
import jade.core.Agent;
import jade.core.behaviours.CyclicBehaviour;
import jade.domain.DFService;
import jade.domain.FIPAException;
import jade.domain.FIPAAgentManagement.DFAgentDescription;
import jade.domain.FIPAAgentManagement.ServiceDescription;
import jade.lang.acl.ACLMessage;
import jade.lang.acl.MessageTemplate;


@SuppressWarnings("serial")
public class PerfectAgent extends Agent {
	
	// Enum fuer den Spielger der gerade am Zug ist
	private enum CurrentPlayer {
		Max, Min
	}
	
	@SuppressWarnings("unused")
	private AID agentId = null;
	private Tree _root = null;
	private RegisterAtStrikemaster registerBeh;
	
	// Initialisiert den Perfectagent
	@Override
	protected void setup() {
		System.out.println(getLocalName()+" STARTED");
		
		// Regestrieren des Agenten mit dem DF
		this.agentId = new AID(getLocalName(), AID.ISLOCALNAME);

		ServiceDescription sd = new ServiceDescription();
		DFAgentDescription dfd = new DFAgentDescription();

		sd.setType(ManagementAgent.SERVICEID_PERFECT_AGENT);
		sd.setName(getName());

		dfd.setName(getAID());
		dfd.addServices(sd);
		
		try {
			// Register with the Directory Facilitator (DF):
			DFService.register(this, dfd);
			System.out.println(getLocalName()+" registered with DF");
		} catch (FIPAException e) {
			System.err.println(getName() + ": Could not register with DF.");
			doDelete(); // Kill the agent
		}
		
		// Add Behavior das nach einem Strikemaster sucht und sich bei ihm regestriert
		registerBeh = new RegisterAtStrikemaster(this);
		addBehaviour(registerBeh);
		
		// Behaviour zum lesen der erhaltenen Nachrichten
		addBehaviour(new CyclicBehaviour(this) {

			@Override
			public void action() {
				// Auslesen der Nachticht
				MessageTemplate mt = MessageTemplate.MatchPerformative(ACLMessage.INFORM);
				ACLMessage msg = myAgent.receive(mt);
				if (msg != null) {
					// INFORM Message received. Process it
					String msgContent = msg.getContent();
					
					// Nachricht wenn der Agent am Zug ist
					if(msgContent.equals(StrikeAgent.YOUR_TURN)){
						System.out.println(getLocalName()+": it's my turn ");
						ACLMessage registerAtMaster = new ACLMessage(ACLMessage.REQUEST);
						
						registerAtMaster.addReceiver(msg.getSender());

						// Fragt den Sender nach der Anzahl der Streichhoelzer
						registerAtMaster.setContent(StrikeAgent.REQUEST_STRIKES);
						myAgent.send(registerAtMaster);
						
					// Nachricht mit der Anzahl an Streichhoelzern
					} else if (msgContent.contains("STRIKES")) {
						int strikes = Integer.parseInt(msgContent.substring(9));
						// Macht einen Zug
						int move = calculateMove(strikes);
						
						System.out.println(getLocalName() + ": received current number of strikes: "+strikes);
						System.out.println(getLocalName() + ": i take " + move);
						
						ACLMessage informMove = new ACLMessage(ACLMessage.INFORM);
						
						informMove.addReceiver(msg.getSender());

						// Antwortet dem Sender mit der Anzahl der gewaehlten Streichhoelzern
						informMove.setContent(StrikeAgent.REPLY_STRIKES + move);
						myAgent.send(informMove);
						
					// Nachricht wenn der Agent geloescht werden soll
					} else if (msgContent.equals(StrikeAgent.DELETE)) {
						myAgent.doDelete();
					
					// Nachricht wenn der Strikemaster geloescht wurden ist
					// dann wird das Regestrieren Behaviour entfernt und wieder
					// neu hinzugefuegt das sich der Agent beim neuen Strikemaster registriert
					} else if (msgContent.contains(StrikeAgent.RESTART)) {
						myAgent.removeBehaviour(registerBeh);
						registerBeh = new RegisterAtStrikemaster(myAgent);
						myAgent.addBehaviour(registerBeh);
					}
				} else {
					block();
				}
			}
		});
    }

	@Override
	protected void takeDown() {
		// Vom DF abmelden
		try {
			DFService.deregister(this);
			System.out.println(getLocalName()+" DEREGISTERED WITH THE DF");
			doDelete();
		} catch (FIPAException e) {
			e.printStackTrace();
		}
	}
	
	private void buildTree(int count) {
		// Neuen Baum erstellen mit Anzahl Streichhoelzer
		_root = new TreeBuilder(count).getTree();
		
		int alpha = -1; 
		int beta = 1;
		// Bewertet den Baum mit Alpha-Beta Pruning
		maxValue(_root, alpha, beta);
	}
	
	// Funktion fuer Alpha-Beta Pruning
	private int maxValue(Tree node, int alpha, int beta) {
		int nodeValue;
		
		// Baut alle moeglichen Kinder des Knotens
		TreeBuilder.build(node);
		// Alle Kinder des uebergebenen Knotens
		for (Tree child : node) {
			// ueberpruefen ob Endknoten.
			if (child.getData() == 0) {
				// Endknoten evaluieren.
				nodeValue = evaluate(child, CurrentPlayer.Min);
			// Kein Endknoten, hat also noch Kinder.
			} else {
				// Kindbewertung durch Min-Methode.
				nodeValue = minValue(child, alpha, beta);
			}
			
			// Der beta-Wert ist kleiner als der aktuelle nodeValue,
			// also fuer Min besser. Demzufolge kann abgebrochen und
			// der beta-Wert zurueckgegeben werden.
			if (nodeValue >= beta) {
				node.setRating(beta);
				return beta;
			}
			
			// Der aktuelle nodeValue ist groesser als der alpha-Wert,
			// also fuer Max besser. Der alpha-Wert wird mit dem
			// nodeValue ueberschrieben.
			if (nodeValue > alpha) {
				alpha = nodeValue;
			}
		}
		
		// Alle Kinder wurden ueberprueft. Der aktuelle alpha-Wert ist
		// der beste fuer Max und wird zurueckgegeben.
		node.setRating(alpha);
		return alpha;
	}

	private int minValue(Tree node, int alpha, int beta) {
		int nodeValue;

		// Baut alle moeglichen Kinder des Knotens
		TreeBuilder.build(node);
		// Alle Kinder des uebergebenen Knotens
		for (Tree child : node) {
			// ueberpruefen ob Endknoten.
			if (child.getData() == 0) {
				// Endknoten evaluieren.
				nodeValue = evaluate(child, CurrentPlayer.Max);
			// Kein Endknoten, hat also noch Kinder.
			} else {
				// Kindbewertung durch Max-Methode.
				nodeValue = maxValue(child, alpha, beta);
			}
			
			// Der alpha-Wert ist kleiner als der aktuelle nodeValue,
			// also fuer Max besser. Demzufolge kann abgebrochen und
			// der alpha-Wert zurueckgegeben werden.
			if (nodeValue <= alpha) {
				node.setRating(alpha);
				return alpha;
			}
			
			// Der aktuelle nodeValue ist groesser als der beta-Wert,
			// also fuer Min besser. Der beta-Wert wird mit dem
			// nodeValue ueberschrieben.
			if (nodeValue < beta) {
				beta = nodeValue;
			}
		}
		
		// Alle Kinder wurden ueberprueft. Der aktuelle beta-Wert ist
		// der beste fuer Min und wird zurueckgegeben.
		node.setRating(beta);
		return beta;
	}
	
	// Bewertet einen Knoten
	private int evaluate(Tree node, CurrentPlayer player) {
		int rating = 0;
		
		if (player == CurrentPlayer.Max) {
			rating = 1;
		} else {
			rating = -1;
		}
		
		node.setRating(rating);
		return rating;
	}
	
	// Waehlt ein Zug
	private int calculateMove(int count) {
		int rating = -10;
		int choice = 0;

		boolean hasNoChild = true;

		// Baut den Baum
		buildTree(count);

		int oldRoot = _root.getData();
		
		// Fuer alle Kinder des Uebergebenen Knotens
		for (Tree child : _root) {
			// Der Kindknoten mit dem besten Rating wird gewaehlt
			// da der Computer Max ist
			if (child.getRating() > rating) {
				rating = child.getRating();
				choice = child.getData();
				_root = child;
				hasNoChild = false;
			}
		}
		
		// Sollte durch Alpha-Beta Pruning ein Node keine Kinder mehr habe
		// Aber es sind noch welche vorhanden wird der Baum neu erstellt
		if (hasNoChild) {
			_root = null;
			buildTree(count);
			choice = calculateMove(count);
		}
		
		// Ausgabe des gewaehlten Zuges
		return (oldRoot - choice);
	}
}